code.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. // @ts-nocheck
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { IS_PLATFORM, useParams } from 'common'
  4. import { isEqual } from 'lodash'
  5. import { AlertCircle, CornerDownLeft, Loader2 } from 'lucide-react'
  6. import { useEffect, useMemo, useState } from 'react'
  7. import { toast } from 'sonner'
  8. import { LogoLoader } from 'ui'
  9. import { DeployEdgeFunctionWarningModal } from '@/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal'
  10. import { formatFunctionBodyToFiles } from '@/components/interfaces/EdgeFunctions/EdgeFunctions.utils'
  11. import { DefaultLayout } from '@/components/layouts/DefaultLayout'
  12. import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
  13. import { PreventNavigationOnUnsavedChanges } from '@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges'
  14. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  15. import { FileExplorerAndEditor } from '@/components/ui/FileExplorerAndEditor'
  16. import { FileData } from '@/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.types'
  17. import { useEdgeFunctionBodyQuery } from '@/data/edge-functions/edge-function-body-query'
  18. import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
  19. import { useEdgeFunctionDeployMutation } from '@/data/edge-functions/edge-functions-deploy-mutation'
  20. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  21. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  22. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  23. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  24. import { BASE_PATH } from '@/lib/constants'
  25. const CodePage = () => {
  26. const { ref, functionSlug } = useParams()
  27. const { data: project } = useSelectedProjectQuery()
  28. const { data: org } = useSelectedOrganizationQuery()
  29. const { mutate: sendEvent } = useSendEventMutation()
  30. const [showDeployWarning, setShowDeployWarning] = useState(false)
  31. const { can: canDeployFunction } = useAsyncCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*')
  32. const { data: selectedFunction } = useEdgeFunctionQuery({
  33. projectRef: ref,
  34. slug: functionSlug,
  35. })
  36. const {
  37. data: functionBody,
  38. isPending: isLoadingFiles,
  39. isError: isErrorLoadingFiles,
  40. isSuccess: isSuccessLoadingFiles,
  41. error: filesError,
  42. } = useEdgeFunctionBodyQuery(
  43. {
  44. projectRef: ref,
  45. slug: functionSlug,
  46. },
  47. {
  48. // [Alaister]: These parameters prevent the function files
  49. // from being refetched when the user is editing the code
  50. retry: false,
  51. retryOnMount: false,
  52. refetchOnWindowFocus: false,
  53. staleTime: Infinity,
  54. refetchOnMount: false,
  55. refetchOnReconnect: false,
  56. refetchInterval: false,
  57. refetchIntervalInBackground: false,
  58. }
  59. )
  60. const [files, setFiles] = useState<FileData[]>([])
  61. const initialFiles = useMemo(() => {
  62. return !!functionBody
  63. ? formatFunctionBodyToFiles({
  64. functionBody,
  65. entrypointPath: selectedFunction?.entrypoint_path,
  66. })
  67. : []
  68. }, [functionBody, selectedFunction?.entrypoint_path])
  69. const { mutate: deployFunction, isPending: isDeploying } = useEdgeFunctionDeployMutation({
  70. onSuccess: () => {
  71. toast.success('Successfully updated edge function')
  72. setShowDeployWarning(false)
  73. setFiles((files) =>
  74. files.map((f) => {
  75. return { ...f, state: 'unchanged' }
  76. })
  77. )
  78. },
  79. })
  80. const fileExists = (filePath: string | undefined): boolean => {
  81. return filePath ? files.some((file) => file.name === filePath) : false
  82. }
  83. const onUpdate = async () => {
  84. if (isDeploying || !ref || !functionSlug || !selectedFunction || files.length === 0) return
  85. try {
  86. const entrypoint_path =
  87. functionBody?.metadata?.deno2_entrypoint_path ?? selectedFunction.entrypoint_path
  88. const newEntrypointPath = entrypoint_path?.split('/').pop()
  89. const newImportMapPath = selectedFunction.import_map_path?.split('/').pop()
  90. const entrypointExists = fileExists(newEntrypointPath)
  91. const importMapExists = fileExists(newImportMapPath)
  92. deployFunction({
  93. projectRef: ref,
  94. slug: selectedFunction.slug,
  95. metadata: {
  96. name: selectedFunction.name,
  97. verify_jwt: selectedFunction.verify_jwt,
  98. ...(entrypointExists && { entrypoint_path: newEntrypointPath }),
  99. ...(importMapExists && { import_map_path: newImportMapPath }),
  100. },
  101. files: files.map(({ name, content }) => ({ name, content })),
  102. })
  103. } catch (error) {
  104. toast.error(
  105. `Failed to update function: ${error instanceof Error ? error.message : 'Unknown error'}`
  106. )
  107. }
  108. }
  109. const handleDeployClick = () => {
  110. if (files.length === 0 || isLoadingFiles) return
  111. setShowDeployWarning(true)
  112. sendEvent({
  113. action: 'edge_function_deploy_updates_button_clicked',
  114. groups: {
  115. project: ref ?? 'Unknown',
  116. organization: org?.slug ?? 'Unknown',
  117. },
  118. })
  119. }
  120. const handleDeployConfirm = () => {
  121. sendEvent({
  122. action: 'edge_function_deploy_updates_confirm_clicked',
  123. groups: {
  124. project: ref ?? 'Unknown',
  125. organization: org?.slug ?? 'Unknown',
  126. },
  127. })
  128. onUpdate()
  129. }
  130. useEffect(() => {
  131. if (initialFiles.length === 0) return
  132. setFiles(initialFiles)
  133. }, [initialFiles])
  134. const hasUnsavedChanges = useMemo(() => {
  135. const normalizeFiles = (list: FileData[]) =>
  136. list.map(({ id, name, content }) => ({ id, name, content }))
  137. return !isEqual(normalizeFiles(initialFiles), normalizeFiles(files))
  138. }, [initialFiles, files])
  139. return (
  140. <div className="flex flex-col h-full">
  141. {isLoadingFiles && (
  142. <div className="flex flex-col items-center justify-center h-full bg-surface-200">
  143. <LogoLoader />
  144. </div>
  145. )}
  146. {isErrorLoadingFiles && (
  147. <div className="flex flex-col items-center justify-center h-full bg-surface-200">
  148. <div className="flex flex-col items-center text-center gap-2 max-w-md">
  149. <AlertCircle size={24} strokeWidth={1.5} className="text-amber-900" />
  150. <h3 className="text-md mt-4">Failed to load function code</h3>
  151. <p className="text-sm text-foreground-light">
  152. {filesError?.message ||
  153. 'There was an error loading the function code. The format may be invalid or the function may be corrupted.'}
  154. </p>
  155. </div>
  156. </div>
  157. )}
  158. {isSuccessLoadingFiles && (
  159. <>
  160. <FileExplorerAndEditor
  161. files={files}
  162. onFilesChange={(files) => {
  163. const formattedFiles: FileData[] = files.map((f) => {
  164. const originalFile = initialFiles.find((x) => x.id === f.id)
  165. if (!originalFile) {
  166. return f
  167. } else if (originalFile.name !== f.name) {
  168. return { ...f, state: 'new' }
  169. } else if (originalFile.content !== f.content) {
  170. return { ...f, state: 'modified' }
  171. }
  172. return { ...f, state: 'unchanged' }
  173. })
  174. setFiles(formattedFiles)
  175. }}
  176. aiEndpoint={`${BASE_PATH}/api/ai/code/complete`}
  177. aiMetadata={{
  178. projectRef: project?.ref,
  179. connectionString: project?.connectionString,
  180. orgSlug: org?.slug,
  181. }}
  182. />
  183. {IS_PLATFORM && (
  184. <div className="flex items-center bg-background-muted justify-end p-4 border-t bg-surface-100 shrink-0">
  185. <ButtonTooltip
  186. loading={isDeploying}
  187. size="medium"
  188. disabled={!canDeployFunction || files.length === 0 || isLoadingFiles}
  189. onClick={handleDeployClick}
  190. iconRight={
  191. isDeploying ? (
  192. <Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
  193. ) : (
  194. <div className="flex items-center space-x-1">
  195. <CornerDownLeft size={10} strokeWidth={1.5} />
  196. </div>
  197. )
  198. }
  199. tooltip={{
  200. content: {
  201. side: 'top',
  202. text: !canDeployFunction
  203. ? 'You need additional permissions to update edge functions'
  204. : undefined,
  205. },
  206. }}
  207. >
  208. Deploy updates
  209. </ButtonTooltip>
  210. </div>
  211. )}
  212. </>
  213. )}
  214. <DeployEdgeFunctionWarningModal
  215. visible={showDeployWarning}
  216. onCancel={() => setShowDeployWarning(false)}
  217. onConfirm={handleDeployConfirm}
  218. isDeploying={isDeploying}
  219. />
  220. <PreventNavigationOnUnsavedChanges hasChanges={hasUnsavedChanges} />
  221. </div>
  222. )
  223. }
  224. CodePage.getLayout = (page: React.ReactNode) => {
  225. return (
  226. <DefaultLayout>
  227. <EdgeFunctionDetailsLayout title="Code">{page}</EdgeFunctionDetailsLayout>
  228. </DefaultLayout>
  229. )
  230. }
  231. export default CodePage